<template>
  <el-form
    :model="formGroup"
    :rules="rules"
    ref="formRef"
    :style="{ width }"
    class="cus__form__container"
    label-width="80px"
  >
    <input type="text" style="display: none" />
    <template v-for="node in nodes" :key="node.key">
      <el-form-item :label="node.label" :prop="node.key" v-if="node.type !== 'hidden'">
        <template v-if="node.type === 'input'">
          <el-input
            clearable
            v-model="formGroup[node.key]"
            :placeholder="node.placeholder || `请输入${node.label}`"
            :disabled="node.disabled"
          />
        </template>
        <template v-else-if="node.type === 'number'">
          <el-input-number
            clearable
            controls-position="right"
            v-model="formGroup[node.key]"
            :min="node.min"
            :max="node.max"
            :step-strictly="node.strictly"
            :step="node.step"
            :placeholder="node.placeholder || `请输入${node.label}`"
          />
        </template>
        <template v-else-if="node.type === 'textarea'">
          <el-input
            clearable
            type="textarea"
            :rows="node.rows || 2"
            v-model="formGroup[node.key]"
            :placeholder="node.placeholder || `请输入${node.label}`"
            :disabled="node.disabled"
          />
        </template>
        <template v-if="node.type === 'password'">
          <el-input
            clearable
            show-password
            v-model="formGroup[node.key]"
            :placeholder="node.placeholder || `请输入${node.label}`"
            :disabled="node.disabled"
          />
        </template>
        <template v-else-if="node.type === 'select'">
          <el-select
            clearable
            :multiple="node.multiple"
            v-model="formGroup[node.key]"
            :placeholder="node.placeholder || `请选择${node.label}`"
            :filterable="node.filterable"
          >
            <el-option
              v-for="option in node.options"
              :key="option[node.valueKey || 'id']"
              :label="option[node.labelKey || 'name']"
              :value="option[node.valueKey || 'id']"
              :disabled="option.disabled"
            />
          </el-select>
        </template>
        <template v-else-if="node.type === 'radio'">
          <el-radio-group v-model="formGroup[node.key]">
            <el-radio
              v-for="option in node.options"
              :key="option[node.valueKey || 'id']"
              :label="option[node.valueKey || 'id']"
              :disabled="option.disabled"
            >{{ option[node.labelKey || "name"] }}</el-radio>
          </el-radio-group>
        </template>
        <template v-else-if="node.type === 'checkbox'">
          <el-checkbox-group v-model="formGroup[node.key]">
            <el-checkbox
              v-for="option in node.options"
              :key="option[node.valueKey || 'id']"
              :label="option[node.valueKey || 'id']"
              :disabled="option.disabled"
            >{{ option[node.labelKey || "name"] }}</el-checkbox>
          </el-checkbox-group>
        </template>
        <template v-else-if="node.type === 'cascader'">
          <el-cascader
            v-model="formGroup[node.key]"
            :options="node.options"
            :placeholder="node.placeholder || `请选择${node.label}`"
            :props="{
              value: node.valueKey || 'id',
              label: node.labelKey || 'name',
              children: node.childKey || 'child',
              lazy: node.lazy || false,
              lazyLoad: node.lazy && node.lazyLoad ? node.lazyLoad : (() => {}),
              checkStrictly: node.checkStrictly || false
            }"
            @change="node.change ? node.change($event, formGroup) : () => {}"
            @expand-change="node.expandChange ? node.expandChange($event, formGroup) : (() => {})"
          />
        </template>
        <template v-else-if="node.type === 'datepicker'">
          <el-date-picker
            type="date"
            :value-format="node.format || 'yyyy-MM-dd'"
            v-model="formGroup[node.key]"
            :placeholder="node.placeholder || `请选择${node.label}`"
          />
        </template>
        <template v-else-if="node.type === 'rangepicker'">
          <el-date-picker
            type="daterange"
            :value-format="node.format || 'yyyy-MM-dd'"
            range-separator="~"
            v-model="formGroup[node.key]"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
          />
        </template>
        <template v-else-if="node.type === 'between'">
          <el-input readonly value="~">
            <template #prepend>
              <el-input-number
                controls-position="right"
                clearable
                style="width: 100%"
                :model-value="formGroup[node?.keys[0]]"
                @update:modelValue="(e) => (formGroup[node?.keys[0]] = e)"
                placeholder="开始"
              />
            </template>
            <template #append>
              <el-input-number
                controls-position="right"
                clearable
                style="width: 100%"
                :model-value="formGroup[node?.keys[1]]"
                @update:modelValue="(e) => (formGroup[node?.keys[1]] = e)"
                placeholder="结束"
              />
            </template>
          </el-input>
        </template>
        <template v-else-if="node.type === 'image'">
          <div class="cus__image__list">
            <template v-if="formGroup[node.key]">
              <div class="cus__image__cell"
                v-for="(image, index) in node.limit === 1 ? [formGroup[node.key]] : formGroup[node.key]">
                <img :src="filePathBase + image" :alt="node.label" />
                <div class="cus__image__actions">
                  <i
                    class="el-icon-delete"
                    @click="node.limit === 1 ? (formGroup[node.key] = null) : formGroup[node.key].replace(index, 1)"
                  />
                </div>
              </div>
            </template>
            <div
              class="cus__upload__btn"
              @click="upload(node)"
              v-if="node.limit > 1 ? formGroup[node.key].length - 1 >= node.limit : !formGroup[node.key]"
            >
              <i class="el-icon-plus" />
            </div>
          </div>
        </template>
        <template v-else>
          <div style="display: none">隐藏项</div>
        </template>
      </el-form-item>
    </template>
  </el-form>
</template>
<script lang="ts">
import { PropType, reactive, ref, watch } from "vue";
import { ElForm, ElFormItem, ElInput, ElInputNumber, ElSelect, ElOption, ElDatePicker, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElCascader, ElSwitch } from "element-plus";
import axios from "axios";
type INodes = PropType<Array<NInput | NNumber | NTextArea | NBetween | NSelect | NCascader | NDatepicker | NRangepicker>>;
export default {
  name: "cus-form",
  props: {
    nodes: {
      type: Array as INodes,
      default: () => [],
    },
    data: {
      type: Object as any,
      default: () => ({}),
    },
    width: {
      type: String,
      default: "100%",
    },
  },
  components: { ElForm, ElFormItem, ElInput, ElInputNumber, ElSelect, ElOption, ElDatePicker, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElCascader, ElSwitch },
  setup(props, { emit }) {
    let formRef = ref();

    let reducer = props.nodes.reduce(
      (collect, node) => {
        if (node.type === "between") {
          collect.formGroup[node.keys[0]] = props.data[node.keys[0]];
          collect.formGroup[node.keys[1]] = props.data[node.keys[1]];
        } else {
          collect.formGroup[node.key] = props.data[node.key] || node.default;
        }
        if (node.type === "select" && node.url) {
          axios.post(node.url).then((res: any) => { node.options = [...res.json, ...(node.options || [])]; });
        }
        if (node.type === "cascader" && node.url) {
          axios.post(node.url).then((res: any) => { node.options = res.json; });
        }
        if (node.rule && node.key) {
          /* ---- 为自定义校验注入formGroup值，用户可在校验规则内获取 formGroup  ---- */
          let rules = Array.isArray(node.rule) ? node.rule : [node.rule];
          collect.rules[node.key] = rules.map((rule) => {
            if (rule.validator) {
              let validate = rule.validator;
              rule.validator = (rule, value, callback) => {
                validate(rule, value, callback, formGroup);
              };
            }
            return rule;
          });
        }
        return collect;
      },
      { formGroup: {}, rules: {} }
    );
    let formGroup = reactive(reducer.formGroup);
    let rules = reactive(reducer.rules);

    let filePathBase = import.meta.env.VITE_APP_BASE_URL;
    const upload = async (node) => {
      let fileDom = document.createElement("input");
      fileDom.setAttribute("type", "file");
      fileDom.setAttribute("accept", ".png,.jpg,.jpeg,.gif");

      fileDom.click();
      fileDom.onchange = async () => {
        let fileList: File[] = Array.from( (fileDom as HTMLInputElement).files || [] );
        let file = fileList[0];
        let formdata = new FormData();
        formdata.append("file", file);
        let res = await axios.post<null, { result: boolean; json: any }>('/system/file/uploadFile', formdata, { headers: { "Content-Type": "multipart/form-data" } } );
        res.result && (formGroup[node.key] = node.limit === 1 ? res.json.filePath : [...(formGroup[node.key] || []), res.json.filePath]);
      };
    };

    const validate = (fn) => {
      if (fn) {
        formRef.value.validate(valid => valid ? fn({ ...formGroup }) : fn(false) );
      } else {
        let result: Promise<any> | null = null;
        formRef.value.validate( valid => { result = valid ? Promise.resolve({ ...formGroup }) : Promise.reject(false) } );
        return result;
      }
    };
    const resetFields = (keys?: string[]) => {
      (keys || Object.keys(formGroup)).map((key) => (formGroup[key] = null));
      setTimeout(() => formRef.value.clearValidate());
    };

    const save = (resolve, reject) => {
      validate((valid) => valid ? resolve(valid) : reject());
    };

    return { formRef, formGroup, rules, validate, resetFields, save, filePathBase, upload };
  },
};

interface NPublic {
  readonly key?: string;
  readonly label: string;
  readonly default?: any;
  readonly placeholder?: string;
  readonly rule?: any;
  readonly change?: (v?) => void;
}
interface NInput extends NPublic {
  readonly type: "input";
  readonly key: string;
}
interface NTextArea extends NPublic {
  readonly type: "input";
  readonly key: string;
  readonly rows?: number;
}
interface NNumber extends NPublic {
  readonly type: "number";
  readonly key: string;
  readonly min: number;
  readonly max: number;
  readonly step?: number;
  readonly strictly?: boolean;
}
interface NBetween extends NPublic {
  readonly type: "between";
  readonly keys: [string, string];
}
interface NSelect extends NPublic {
  readonly type: "select";
  readonly key: string;
  readonly url?: string;
  readonly multiple?: boolean;
  options?: any[];
  readonly labelKey?: string;
  readonly valueKey?: string;
  readonly filterable?: boolean;
}
interface NCascader extends NPublic {
  readonly type: "cascader";
  readonly key: string;
  readonly url?: string;
  readonly multiple?: boolean;
  options?: any[];
  readonly labelKey?: string;
  readonly valueKey?: string;
  readonly childKey?: string;
  readonly lazy?: boolean;
  readonly lazyLoad?: () => void
  readonly checkStrictly?: boolean;
  readonly expandChange?: () => void
}
interface NDatepicker extends NPublic {
  readonly type: "datepicker";
  readonly key: string;
  format?: "yyyy-MM-dd" | "yyyy-MM-dd hh:mm:ss";
}
interface NRangepicker extends NPublic {
  readonly type: "rangepicker";
  readonly key: string;
  format?: "yyyy-MM-dd" | "yyyy-MM-dd hh:mm:ss";
}
interface NImage extends NPublic {
  readonly type: "image";
  readonly key: string;
  readonly limit: number;
}
</script>
<style lang="scss" scoped>
$imageSize: 100px;
.cus__form__container {
  .el-select,
  :deep(.el-cascader),
  :deep(.el-input-number) {
    width: 100%;
    input {
      text-align: left;
    }
  }
  .el-input-group {
    display: flex;
    .el-input-group__prepend,
    .el-input-group__append,
    & > input {
      display: block;
    }
    & > input {
      width: 40px;
      border-left: 0;
      border-right: 0;
      pointer-events: none;
    }
    .el-input-group__prepend,
    .el-input-group__append {
      flex: 1;
      padding: 0;
      border-width: 0;
    }
    .el-input-group__prepend input {
      border-top-left-radius: 4px;
      border-bottom-left-radius: 4px;
      text-align: left;
    }
    .el-input-group__append input {
      border-top-right-radius: 4px;
      border-bottom-right-radius: 4px;
      text-align: left;
    }
  }
  .cus__image__list {
    display: flex;
    & > div {
      margin: 0 12px 12px 0;
    }
    .cus__image__cell {
      width: $imageSize;
      height: $imageSize;
      border-radius: 4px;
      border: solid 1px #dcdfe6;
      background: #fff;
      transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
      position: relative;
      overflow: hidden;
      & > img {
        display: block;
        width: 100%;
        height: 100%;
        max-width: 100%;
        max-height: 100%;
      }
      &:hover .cus__image__actions {
        opacity: 1;
        visibility: visible;
      }
      .cus__image__actions {
        width: 100%;
        height: 100%;
        line-height: $imageSize;
        text-align: center;
        background: rgba($color: #000000, $alpha: 0.5);
        position: absolute;
        top: 0;
        left: 0;
        opacity: 0;
        visibility: hidden;
        transition: all 0.25s;
        & > i {
          color: #fff;
          font-size: 20px;
          cursor: pointer;
        }
      }
    }
    .cus__upload__btn {
      width: $imageSize;
      height: $imageSize;
      line-height: $imageSize;
      color: #999;
      font-size: 28px;
      font-weight: 400;
      text-align: center;
      border-radius: 4px;
      border: solid 1px #dcdfe6;
      background: #fff;
      transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
      cursor: pointer;
      &:hover {
        border-color: #c0c4cc;
      }
    }
  }
}
</style>
